如果你來自 C++ 世界,你對多執行緒的印象大概是:
“能編譯就能跑,只是結果可能不對。”
Rust 的哲學剛好相反:
“能跑就表示結果一定對——因為不對的根本不能編譯。”
Rust 的「跨執行緒安全」靠兩個核心 trait 保證:
這兩個 trait 幾乎是 Rust 並行模型的憲法。
Send:如果型別 T 是 Send,表示它的擁有權可以安全移交到另一條執行緒。Sync:如果型別 T 是 Sync,表示你可以同時在多執行緒間「共享 &T(不可變借用)」。用公式表示:
T: Send → 可跨執行緒 move
&T: Sync → 可跨執行緒共享引用
例如:
use std::thread;
let v = vec![1, 2, 3];
thread::spawn(move || {
println!("{:?}", v);
}).join().unwrap(); // OK
Vec<T> 是 Send 的,所以可以被 move 進另一條 thread。
但以下這段會 編譯失敗:
use std::rc::Rc;
use std::thread;
let r = Rc::new(42);
thread::spawn(move || {
println!("{r}");
});
錯誤訊息:
Rc<i32> cannot be sent between threads safely
因為 Rc<T> 不是 Send(它的引用計數不是 atomic),如果多執行緒同時操作,會造成 race condition。Rust 不允許這樣的型別跨線。
你不需要手動實作,大部分標準型別都自動實作了這兩個 trait。
| 類型 | Send | Sync | 說明 |
|---|---|---|---|
i32, f64, bool, String, Vec<T> |
✅ | ✅ | 值語意,可安全複製或共享 |
Rc<T> |
❌ | ❌ | 非 thread-safe RC |
Arc<T> |
✅ | ✅ | 原子計數,可跨線 |
Mutex<T> |
✅ | ✅ | 鎖住內部 T,保證排他存取 |
RefCell<T> |
❌ | ❌ | 僅限單執行緒 runtime 借用檢查 |
Cell<T> |
❌ | ❌ | 同上 |
*mut T / *const T |
❌ | ❌ | 原始指標,不具安全語義 |
Rust 編譯器會自動根據型別內含內容決定能不能跨線。
這一點與 C++ 相反——在 C++,所有東西理論上都能跨線;在 Rust,只有被認證安全的才行。
要共享資料怎麼辦?在 Rust,你不能用 Rc,要用:
一起用時的典型寫法:
use std::sync::{Arc, Mutex};
use std::thread;
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..5 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for h in handles { h.join().unwrap(); }
println!("result = {}", *counter.lock().unwrap());
Arc::new(Mutex::new(0))Arc::clone.lock()這種組合 (Arc<Mutex<T>>) 幾乎是所有跨線共享狀態的起點。
而且它自動實作 Send、Sync,所以不需要 unsafe。
Rust 的 borrow checker 會擋掉任何可能造成資料競爭的型別:
| 型別 | 為什麼不能跨線? |
|---|---|
Rc<T> |
非原子計數,多線 race |
RefCell<T> |
借用檢查在 runtime,不 thread-safe |
Cell<T> |
同上,只適合單線修改 |
*mut T / *const T |
原始指標,不知道誰擁有 |
&mut T |
可變引用只能唯一存在,跨線會破壞規則 |
這些型別在單執行緒下非常好用(例如 GUI event loop 或 wasm),
但只要放進 thread::spawn,編譯器就會馬上報紅。
Rust 把「data race」變成編譯錯誤,而不是 runtime 驚喜。
這是 Rust 想讓你思考的問題。
在 C++,我們容易先開 thread 再想同步;
Rust 則鼓勵你先想「擁有權的分工」。
一個常見的 pattern 是:
Mutex<T>;Arc<T>;std::sync::mpsc 或 tokio channel)。Rust 的並行模型更接近「資料流分工」而不是「共享狀態爭奪」。
你不需要鎖全域變數,只需要把資料送給下一個擁有者。
| 面向 | C++ | Rust |
|---|---|---|
| 執行緒建立 | 任意傳參 | move 進閉包需滿足 Send |
| 鎖 | 任何 mutex 都能鎖任何物件 | 需 Send + 'static 才能跨線 |
| RC 計數 | shared_ptr 不分執行緒 |
Rc 限單線,Arc 限跨線 |
| 借用檢查 | runtime/手動紀律 | 編譯期強制 |
| 錯誤型態 | race → UB/crash | race → compile error |